/*******************************************************************************
 * Copyright 2010 Ben Knowles
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package com.midtro.attolog;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;

/**
 * Loads the logger configuration and manages all of the stream writing.
 * 
 * Adapted from the 'SimpleLog' class.
 * 
 * @author Ben Knowles
 */
final class LoggerManager {

	private static final LoggerManager INSTANCE = new LoggerManager();
	private final DateFormat dateFormat = new SimpleDateFormat("EEE yyyy/MM/dd HH:mm:ss.SSS");
	private final DateFormat fileDateFormat = new SimpleDateFormat("yyyy-MM-dd");
	private final MessageFormat messageFormat = new MessageFormat("{0}|   |{1}|{2}|{4}");
	private final MessageFormat objectFormat = new MessageFormat("{0}|---|{1}|{2}|{4}|{5}");
	private final MessageFormat exceptionFormat = new MessageFormat("{0}|***|{1}|{2}|{4}");
	private final Properties configuration = new Properties();
	private BufferedWriter fileWriter;
	private boolean logToConsole = true;
	
	/**
	 * Initializes the message formats and loads the logger configuration.
	 */
	private LoggerManager(){
		this.messageFormat.setFormatByArgumentIndex(0, this.dateFormat);
		this.objectFormat.setFormatByArgumentIndex(0, this.dateFormat);
		this.exceptionFormat.setFormatByArgumentIndex(0, this.dateFormat);
		this.exceptionFormat.setFormatByArgumentIndex(4, new LoggerException());
		this.loadConfiguration();
	}
	
	/**
	 * Attempts to load the properties from the filesystem.
	 */
	private void loadConfiguration(){
		try {
			final File file = new File("logging.properties");
			
			if(file.exists()){
				// Read the configuration from the file
				configuration.load(new BufferedReader(new FileReader(file)));
				
				// Handle file output option
				if(configuration.containsKey("output.file")){
					try {
						final String path = formatPath(configuration.getProperty("output.file"));
						final File logFile = new File(path);
						
						// Create log file if it doesn't exist
						if(!logFile.exists() && !logFile.createNewFile()){
							throw new IOException("Unable to create log file");
						}
						
						// Initialize writer
						this.fileWriter = new BufferedWriter(new FileWriter(logFile, true));
					} catch(IOException lfe){
						printError("Unable to initialize filewriter -- will not write to file", lfe);
					}
				}
				
				// Handle console output option
				if(configuration.containsKey("output.console")){
					logToConsole = Boolean.parseBoolean(configuration.getProperty("output.console"));
				}
			} else {
				// No configuration file could be loaded, so instead
				// we'll go into silent mode, which is preferrable
				// for production applications.
				logToConsole = false;
			}
		} catch(SecurityException se){
			// Can't access file system due to security restrictions
			// This is most likely in an unsigned applet or the user
			// has set restrictions on IO in their Java settings.
		} catch(IOException e){
			printError("Error setting up logger", e);
		}
	}
	
	/**
	 * Prints a line to the console if enabled, and to the file writer if enabled.
	 * @param line The string to print.
	 * @param src The logger that this message is coming from.
	 */
	void println(String line, AttoLogger src){
		// Write to console
		if(logToConsole){
			System.err.println(line);
		}
		
		// Write to file
		if(fileWriter != null){
			try {
				fileWriter.write(line);
				fileWriter.newLine();
				fileWriter.flush();
			} catch(IOException e){
				printError("Error writing to file", e);
			}
		}
	}
	
	/**
	 * Configures the logger by iterating through all of the tokens in the package name.
	 * @param logger The logger to configure.
	 */
	void configure(AttoLogger logger){
		String configName = logger.getConfigName();
		Level level = null;
		int dot = configName.length();
		
		do {
			configName = configName.substring(0, dot);
			
			if(configuration.containsKey(configName)){
				final String value = configuration.getProperty(configName).trim();
				
				level = Level.forName(value);
				
				if(level == null){
					try {
						level = Level.forID(Integer.parseInt(value));
					} catch(NumberFormatException nfe){
						printError("Invalid property value for '"+configName+"'", nfe);
					}
				}
			}
			
			dot = configName.lastIndexOf('.');
		} while(level == null && dot != -1);
		
		// Set the level to the one we detected above.
		// Otherwise, it will be the default which is INFO.
		if(level != null){
			logger.setLevel(level);
		}
	}
	
	/**
	 * Formats the log file path if it includes a date parameter.
	 * @param path The input path string.
	 * @return The formatted path.
	 */
	private String formatPath(String path){
		if(path == null){
			throw new NullPointerException("Path cannot be null");
		}
		
		// Replace the date parameter with the current date
		if(path.contains("{0}")){
			final MessageFormat format = new MessageFormat(path);
			format.setFormatByArgumentIndex(0, fileDateFormat);
			return format.format(new Object[]{new Date()});
		}
		
		return path;
	}
	
	/**
	 * Prints an error message that the logger has encountered.
	 * @param message The error message, which can be null.
	 * @param throwable The throwable, which can be null.
	 */
	private void printError(String message, Throwable throwable){
		if(message != null){
			System.err.println("AttoLogger error: "+message);
		}
		
		if(throwable != null){
			throwable.printStackTrace(System.err);
		}
	}
	
	/**
	 * Gets the format used by normal messages.
	 */
	MessageFormat getMessageFormat(){
		return messageFormat;
	}
	
	/**
	 * Gets the format used by object inspection messages.
	 */
	MessageFormat getObjectFormat(){
		return objectFormat;
	}
	
	/**
	 * Gets the format used by exception messages.
	 */
	MessageFormat getExceptionFormat(){
		return exceptionFormat;
	}
	
	/**
	 * Gets the logger manager singleton.
	 */
	public static LoggerManager getInstance(){
		return INSTANCE;
	}
}
